﻿using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Windows.Media;
using Notoric.Commanding.CommandBase;

namespace Notoric.ViewModel.Elements.CWNotation
{
    /// <summary>
    /// Adapts a <see cref="Model.Elements.CWNotation.Staff"/> instance to a Windows Presentation Foundation view.
    /// </summary>
    public class Staff : ViewModelElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Staff"/> instance.
        /// </summary>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="System"/>
        /// </para>
        /// <para>
        /// The system that directly owns this staff.
        /// </para>
        /// </param>
        /// <param name="adaptedStaff">
        /// <para>
        /// Type: <see cref="Model.Elements.CWNotation.Staff"/>
        /// </para>
        /// <para>
        /// The adapted staff instance.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="adaptedStaff"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="NullReferenceException">
        /// <paramref name="owner"/> is <see langword="null"/>.
        /// The values of the <see cref="ViewModelElement.UndoStack"/> and <see cref="ViewModelElement.RedoStack"/> properties
        /// are passed directly to the base class (<see cref="ViewModelElement"/>) constructor. Thus, there is not
        /// an occasion to react with a more appropriate exception.
        /// </exception>
        public Staff(System owner, Model.Elements.CWNotation.Staff adaptedStaff)
            : base(adaptedStaff, owner, owner.UndoStack, owner.RedoStack)
        {
            lines = new ObservableCollection<StaffLine>();

            for (int i = 0; i < adaptedStaff.Descriptor.Lines; ++i)
                lines.Add(new StaffLine(this, i));

            adaptedStaff.Descriptor.PropertyChanged += new PropertyChangedEventHandler(Descriptor_PropertyChanged);    

            RegisterToOwnerPropertyChanged<System>();
        }

        #endregion

        #region ViewModelElement Members

        /// <inheritdoc/>
        public override void Dispose()
        {
            ((Model.Elements.CWNotation.Staff)AdaptedElement).Descriptor.PropertyChanged -= Descriptor_PropertyChanged;

            foreach (var line in lines)
                line.Dispose();
            foreach (var symbol in symbols)
                symbol.Dispose();

            base.Dispose();
        }

        /// <inheritdoc/>
        public override string ElementNameKey
        {
            get { return elementNameKey; }
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the value of some property of the descriptor of the adapted staff has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Descriptor_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var descriptor = (Model.Elements.CWNotation.StaffDescriptor)sender;

            if (e.PropertyName == "Lines")
            {
                if (lines.Count > descriptor.Lines)
                {
                    for (int i = lines.Count - 1; i >= descriptor.Lines; --i)
                        lines.RemoveAt(i);
                }
                else if (lines.Count < descriptor.Lines)
                {
                    for (int i = lines.Count; i < descriptor.Lines; ++i)
                        lines.Add(new StaffLine(this, i));
                }
            }
        }

        /// <inheritdoc/>
        protected override bool AdaptedElement_PropertyChanged(PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Top")
                RaisePropertyChanged("Bottom");

            return base.AdaptedElement_PropertyChanged(e);
        }

        /// <inheritdoc/>
        protected override void Owner_PropertyChanged(ViewModelElement owner, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Top")
            {
                RaisePropertyChanged("Top");
                RaisePropertyChanged("Bottom");
            }
        }

        /// <summary>
        /// Occurs when the symbols collection of the adapted staff has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Symbols_CollectionChanged(object sender, Model.Collections.NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case Model.Collections.NotifyCollectionChangedAction.Add:
                {
                    int i = 0;

                    foreach (Model.Elements.CWNotation.CoreSymbol symbol in e.NewItems)
                    {
                        symbols.Insert(e.NewStartingIndex + i, CoreSymbol.CreateAdapter(this, symbol));
                        ++i;
                    }
                    break;
                }
                case Model.Collections.NotifyCollectionChangedAction.Move:
                    symbols.Move(e.OldStartingIndex, e.NewStartingIndex);
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Remove:
                    foreach (Model.Elements.CWNotation.CoreSymbol symbol in e.OldItems)
                        symbols.Remove(symbols.First(s => s.AdaptedElement == symbol));
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Reset:
                    symbols.Clear();
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Replace:
                {
                    foreach (Model.Elements.CWNotation.CoreSymbol symbol in e.OldItems)
                        symbols.Remove(symbols.First(s => s.AdaptedElement == symbol));
                    
                    int i = 0;

                    foreach (Model.Elements.CWNotation.CoreSymbol symbol in e.NewItems)
                    {
                        symbols.Insert(e.NewStartingIndex + i, CoreSymbol.CreateAdapter(this, symbol));
                        ++i;
                    }
                }
                    break;
            }
        }

        /// <summary>
        /// Occurs when the attachments collection of the adapted staff has changed.
        /// </summary>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void Attachments_CollectionChanged(object sender, Model.Collections.NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case Model.Collections.NotifyCollectionChangedAction.Add:
                {
                    int i = 0;

                    foreach (Model.Elements.CWNotation.Attachment symbol in e.NewItems)
                    {
                        attachments.Insert(e.NewStartingIndex + i, Attachment.CreateAdapter(this, symbol));
                        ++i;
                    }
                    break;
                }
                case Model.Collections.NotifyCollectionChangedAction.Move:
                    attachments.Move(e.OldStartingIndex, e.NewStartingIndex);
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Remove:
                    foreach (Model.Elements.CWNotation.Attachment symbol in e.OldItems)
                        attachments.Remove(attachments.First(s => s.AdaptedElement == symbol));
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Reset:
                    attachments.Clear();
                    break;
                case Model.Collections.NotifyCollectionChangedAction.Replace:
                {
                    foreach (Model.Elements.CWNotation.Attachment symbol in e.OldItems)
                        attachments.Remove(attachments.First(s => s.AdaptedElement == symbol));

                    int i = 0;

                    foreach (Model.Elements.CWNotation.Attachment symbol in e.NewItems)
                    {
                        attachments.Insert(e.NewStartingIndex + i, Attachment.CreateAdapter(this, symbol));
                        ++i;
                    }
                    break;
                }
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates an undoable command for setting the <see cref="Color"/> of the staff.
        /// </summary>
        /// <param name="color">
        /// <para>
        /// Type: <see cref="Color"/>
        /// </para>
        /// <para>
        /// The new color of the staff.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetColorCommand(Color color)
        {
            var modelColor = (Model.Elements.Color)StructuresConverter.ConvertBack(color, typeof(Model.Elements.Color), null, CultureInfo.CurrentCulture);

            return new SetterCommand<Model.Elements.Color>(this, "Color", modelColor, UndoStack, RedoStack);
        }

        /// <summary>
        /// Creates an undoable command for setting the <see cref="Height"/> of the staff.
        /// </summary>
        /// <param name="height">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The new height (in device independent pixels) of the staff.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetHeightCommand(double height)
        {
            var modelHeight = (double)LengthConverter.ConvertBack(height, typeof(double), null, CultureInfo.CurrentCulture);

            return new SetterCommand<double>(this, "Height", modelHeight, UndoStack, RedoStack);
        }

        /// <summary>
        /// Creates an undoable command for setting the <see cref="Top"/> of the staff.
        /// </summary>
        /// <param name="top">
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The new offset (in device independent pixels) of the top staff line from the top of the system.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="UndoableCommandBase"/>
        /// </para>
        /// <para>
        /// The created command ready for execution.
        /// </para>
        /// </returns>
        public UndoableCommandBase CreateSetTopCommand(double top)
        {
            var modelTop = (double)LengthConverter.ConvertBack(top, typeof(double), null, CultureInfo.CurrentCulture);

            return new SetterCommand<double>(this, "Top", modelTop, UndoStack, RedoStack);
        }

        /// <summary>
        /// Converts a given staff position to the offset (in device independent pixels) from the <see cref="Top"/> of the staff.
        /// </summary>
        /// <param name="staffPosition">
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The staff position to convert.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The computed offset.
        /// </para>
        /// </returns>
        public double GetVerticalOffset(int staffPosition)
        {
            var topRelativePos = (lines.Count > 0 ? staffPosition - ((lines.Count - 1) * 2) : staffPosition);

            return -topRelativePos * (SpaceSize / 2);
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the collection of attachments.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ObservableCollection{T}"/> of <see cref="Attachment"/>
        /// </para>
        /// <para>
        /// The collection of attachments.
        /// </para>
        /// </value>
        public ObservableCollection<Attachment> Attachments
        {
            get 
            {
                if (attachments == null)
                {
                    var adaptedStaff = (Model.Elements.CWNotation.Staff)AdaptedElement;

                    attachments = new ObservableCollection<Attachment>();
                    foreach (Model.Elements.CWNotation.Attachment symbol in adaptedStaff.Attachments)
                        attachments.Add(Attachment.CreateAdapter(this, symbol));
                    adaptedStaff.Attachments.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(Attachments_CollectionChanged);
                }
                return attachments; 
            }
        }

        /// <summary>
        /// Gets the bottom of the staff.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The distance (in device independent pixels) between the bottom (first) staff line
        /// and the top section's edge.
        /// </para>
        /// </value>
        public double Bottom
        {
            get { return Top + Height; }
        }

        /// <summary>
        /// Gets or sets the color of the staff.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Color"/>
        /// </para>
        /// <para>
        /// The color of the staff.
        /// </para>
        /// </value>
        public Color Color
        {
            get 
            {
                return (Color)StructuresConverter.Convert(GetPropertyValue<Model.Elements.Color>("Color"), typeof(Color), null, CultureInfo.CurrentCulture);
            }
            set 
            {
                if (UnsavedChanges.ContainsKey("Color"))
                {
                    var cmd = (SetterCommand<Model.Elements.Color>)UnsavedChanges["Color"];
                    cmd.Value = (Model.Elements.Color)StructuresConverter.ConvertBack(value, typeof(Model.Elements.Color), null, CultureInfo.CurrentCulture);
                }
                else
                    UnsavedChanges["Color"] = CreateSetColorCommand(value);

                RaisePropertyChanged("Color");
            }
        }

        /// <summary>
        /// Gets or sets the height of the staff.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The distance (in device independent pixels) between the top and bottom staff line.
        /// </para>
        /// </value>
        public double Height
        {
            get
            {
                return (double)LengthConverter.Convert(GetPropertyValue<double>("Height"), typeof(double), null, CultureInfo.CurrentCulture);
            }
            set
            {
                if (UnsavedChanges.ContainsKey("Height"))
                {
                    var cmd = (SetterCommand<double>)UnsavedChanges["Height"];
                    cmd.Value = (double)LengthConverter.ConvertBack(value, typeof(double), null, CultureInfo.CurrentCulture);
                }
                else
                    UnsavedChanges["Height"] = CreateSetHeightCommand(value);

                RaisePropertyChanged("Height");
                RaisePropertyChanged("Bottom");
            }
        }

        /// <summary>
        /// Gets the collection of staff lines.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ObservableCollection{T}"/> of <see cref="StaffLine"/>
        /// </para>
        /// <para>
        /// The collection of lines that form this staff. This collection contains aligned staff lines
        /// so that a data template in a view can directly bind to them.
        /// </para>
        /// <para>
        /// The content of this collection should not be modified from outside.
        /// It is implemented by the <see cref="ObservableCollection{T}"/> to allow the view to listen
        /// to the collection changed event. This collection reflects possible changes to the Lines property
        /// of the adapted staff. However, it doesnot propagate any change back to the model.
        /// </para>
        /// </value>
        public ObservableCollection<StaffLine> Lines
        {
            get { return lines; }
        }

        /// <summary>
        /// Gets the space size.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The distance (in device independent pixels) between two consecutive staff lines.
        /// </para>
        /// </value>
        public double SpaceSize
        {
            get { return (double)LengthConverter.Convert(((Model.Elements.CWNotation.Staff)AdaptedElement).SpaceSize, typeof(double), null, CultureInfo.CurrentCulture); }
        }

        /// <summary>
        /// Gets the collection of symbols.
        /// </summary>
        public ObservableCollection<CoreSymbol> Symbols
        {
            get 
            {
                if (symbols == null)
                {
                    var adaptedStaff = (Model.Elements.CWNotation.Staff)AdaptedElement;

                    symbols = new ObservableCollection<CoreSymbol>();
                    foreach (Model.Elements.CWNotation.CoreSymbol symbol in adaptedStaff.Symbols)
                        symbols.Add(CoreSymbol.CreateAdapter(this, symbol));

                    adaptedStaff.Symbols.CollectionChanged += new EventHandler<Model.Collections.NotifyCollectionChangedEventArgs>(Symbols_CollectionChanged);
                }
                return symbols; 
            }
        }

        /// <summary>
        /// Gets or sets the distance of the top staff line from the top of the system.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The distance (in centimeters) between the top staff line and top of the owner system.
        /// </para>
        /// </value>
        public double Top
        {
            get
            {
                double top = GetPropertyValue<double>("Top");

                return FindOwner<System>().Top + (double)LengthConverter.Convert(top, typeof(double), null, CultureInfo.CurrentCulture);
            }
            set
            {
                if (UnsavedChanges.ContainsKey("Top"))
                {
                    var cmd = (SetterCommand<double>)UnsavedChanges["Top"];
                    cmd.Value = (double)LengthConverter.ConvertBack(value, typeof(double), null, CultureInfo.CurrentCulture);
                }
                else
                    UnsavedChanges["Top"] = CreateSetTopCommand(value);

                RaisePropertyChanged("Top");
                RaisePropertyChanged("Bottom");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Attachments"/> property.
        /// </summary>
        private ObservableCollection<Attachment> attachments;
        /// <summary>
        /// Holds a user-friendly name of this element.
        /// </summary>
        private const string elementNameKey = "ViewModel.ElementNames.Staff";
        /// <summary>
        /// Holds the value of the <see cref="Lines"/> property.
        /// </summary>
        private ObservableCollection<StaffLine> lines;
        /// <summary>
        /// Holds the value of the <see cref="Symbols"/> property.
        /// </summary>
        private ObservableCollection<CoreSymbol> symbols;

        #endregion
    }
}
